package org.nokatag.temp;



import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.nokatag.system.Concest;


public class MiniTemplator {

//--- exceptions -----------------------------------------------------

/**
* Thrown when a syntax error is encountered within the template.
*/
public static class TemplateSyntaxException extends RuntimeException {
private static final long serialVersionUID = 1;
public TemplateSyntaxException (String msg) {
   super ("Syntax error in template: " + msg); }}

/**
* Thrown when {@link MiniTemplator#setVariable(String, String, boolean) Minitemplator.setVariable}
* is called with a <code>variableName</code> that is not defined
* within the template and the <code>isOptional</code> parameter is <code>false</code>.
*/
public static class VariableNotDefinedException extends RuntimeException {
private static final long serialVersionUID = 1;
public VariableNotDefinedException (String variableName) {
   super ("Variable \"" + variableName + "\" not defined in template."); }}

/**
* Thrown when {@link MiniTemplator#addBlock Minitemplator.addBlock}
* is called with a <code>blockName</code> that is not defined
* within the template.
*/
public static class BlockNotDefinedException extends RuntimeException {
private static final long serialVersionUID = 1;
public BlockNotDefinedException (String blockName) {
   super ("Block \"" + blockName + "\" not defined in template."); }}

//--- public nested classes ------------------------------------------

/**
* Specifies the parameters for constructing a {@link MiniTemplator} object.
*/
public static class TemplateSpecification {                // template specification
/**
* The file name of the template file.
*/
public String             templateFileName;
/**
* The path of the base directory for reading subtemplate files.
* This path is used to convert the relative paths of subtemplate files (specified with the $include commands)
* into absolute paths.
* If this field is null, the parent directory of the main template file (specified by <code>templateFileName</code>) is used.
*/
public String             subtemplateBasePath;
/**
* The character set to be used for reading and writing files.
* This charset is used for reading the template and subtemplate files and for
* writing output with {@link #generateOutput(String outputFileName)}.
* If this field is null, the default charset of the Java VM is used.
*/
public Charset            charset;
/**
* The contents of the template file.
* This field may be used instead of <code>templateFileName</code> to pass the template text in memory.
* If this field is not null, <code>templateFileName</code> will be ignored.
*/
public String             templateText;
/**
* Flags for the conditional commands ($if, $elseIf).
* A set of flag names, that can be used with the $if and $elseIf commands.
* The flag names are case-insensitive.
*/
public Set<String>        conditionFlags; }

//--- private nested classes -----------------------------------------

private static class BlockDynTabRec {                      // block dynamic data table record structure
int                       instances;                    // number of instances of this block
int                       firstBlockInstNo;             // block instance no of first instance of this block or -1
int                       lastBlockInstNo;              // block instance no of last instance of this block or -1
int                       currBlockInstNo; }            // current block instance no, used during generation of output file
private static class BlockInstTabRec {                     // block instance table record structure
int                       blockNo;                      // block number
int                       instanceLevel;                // instance level of this block
   // InstanceLevel is an instance counter per block.
   // (In contrast to blockInstNo, which is an instance counter over the instances of all blocks)
int                       parentInstLevel;              // instance level of parent block
int                       nextBlockInstNo;              // pointer to next instance of this block or -1
   // Forward chain for instances of same block.
String[]                  blockVarTab; }                // block instance variables

//--- private variables ----------------------------------------------

private MiniTemplatorParser  mtp;                          // contains the parsed template
private Charset              charset;                      // charset used for reading and writing files
private String               subtemplateBasePath;          // base path for relative file names of subtemplates, may be null

private String[]             varValuesTab;                 // variable values table, entries may be null

private BlockDynTabRec[]     blockDynTab;                  // dynamic block-specific values
private BlockInstTabRec[]    blockInstTab;                 // block instances table
// This table contains an entry for each block instance that has been added.
// Indexed by BlockInstNo.
private int                  blockInstTabCnt;              // no of entries used in BlockInstTab

//--- constructors ---------------------------------------------------

/**
* Constructs a MiniTemplator object.
* <p>During construction, the template and subtemplate files are read and parsed.
* <p>Note: The {@link MiniTemplatorCache} class may be used to cache MiniTemplator objects.
* @param  templateSpec             the template specification.
* @throws TemplateSyntaxException  when a syntax error is detected within the template.
* @throws IOException              when an i/o error occurs while reading the template.
*/
public MiniTemplator (TemplateSpecification templateSpec)
   throws IOException, TemplateSyntaxException {
init (templateSpec); }

/**
* Constructs a MiniTemplator object by specifying only the file name.
* <p>This is a convenience constructor that may be used when only the file name has to be specified.
* @param  templateFileName         the file name of the template file.
* @throws TemplateSyntaxException  when a syntax error is detected within the template.
* @throws IOException              when an i/o error occurs while reading the template.
* @see #MiniTemplator(TemplateSpecification)
*/
public MiniTemplator (String templateFileName)throws IOException, TemplateSyntaxException {
TemplateSpecification templateSpec = new TemplateSpecification();
templateSpec.templateFileName = templateFileName;
init (templateSpec); }

public MiniTemplator(InputStream input)throws IOException, TemplateSyntaxException {
	TemplateSpecification templateSpec = new TemplateSpecification();
	InputStreamReader reader=null;
	try {
		reader = new InputStreamReader(input,Charset.forName(Concest.ENCODING));
		templateSpec.templateText= readStreamIntoString(reader); 
	}finally {
		if (reader != null) reader.close();
		if (input != null) input.close(); 
	}
	init (templateSpec); 
}
private void init (TemplateSpecification templateSpec)throws IOException, TemplateSyntaxException {
charset = templateSpec.charset;
if (charset == null) charset = Charset.forName(Concest.ENCODING);
subtemplateBasePath = templateSpec.subtemplateBasePath;
if (subtemplateBasePath == null && templateSpec.templateFileName != null)
   subtemplateBasePath = new File(templateSpec.templateFileName).getParent();
String templateText = templateSpec.templateText;
if (templateText == null && templateSpec.templateFileName != null)
   templateText = readFileIntoString(templateSpec.templateFileName);
if (templateText == null) throw new IllegalArgumentException("No templateFileName or templateText specified.");
mtp = new MiniTemplatorParser(templateText, templateSpec.conditionFlags, this);
reset(); }

//Private dummy constructor, used for cloneReset().
private MiniTemplator() {}

//--- loadSubtemplate ------------------------------------------------

/**
* Loads the template string of a subtemplate (used for the $Include command).
* This method can be overridden in a subclass, to load subtemplates from
* somewhere else, e.g. from a database.
* <p>This implementation of the method interprets <code>subtemplateName</code>
* as a relative file path name and reads the template string from that file.
* {@link MiniTemplator.TemplateSpecification#subtemplateBasePath} is used to convert
* the relative path of the subtemplate into an absolute path.
* @param  subtemplateName     the name of the subtemplate.
*        Normally a relative file path.
*        This is the argument string that was specified with the "$Include" command.
*        If the string has quotes, the quotes are removed before this method is called.
* @return the template text string of the subtemplate.
**/
protected String loadSubtemplate (String subtemplateName) throws IOException {
String fileName = new File(subtemplateBasePath, subtemplateName).getPath();
return readFileIntoString(fileName); }

//--- build up (template variables and blocks) ------------------------

/**
* Resets the MiniTemplator object to the initial state.
* All variable values are cleared and all added block instances are deleted.
* This method can be used to produce another HTML page with the same
* template. It is faster than creating another MiniTemplator object,
* because the template does not have to be read and parsed again.
*/
public void reset() {
if (varValuesTab == null)
   varValuesTab = new String[mtp.varTabCnt];
 else
   for (int varNo=0; varNo<mtp.varTabCnt; varNo++)
      varValuesTab[varNo] = null;
if (blockDynTab == null)
   blockDynTab = new BlockDynTabRec[mtp.blockTabCnt];
for (int blockNo=0; blockNo<mtp.blockTabCnt; blockNo++) {
   BlockDynTabRec bdtr = blockDynTab[blockNo];
   if (bdtr == null) {
      bdtr = new BlockDynTabRec();
      blockDynTab[blockNo] = bdtr; }
   bdtr.instances = 0;
   bdtr.firstBlockInstNo = -1;
   bdtr.lastBlockInstNo = -1; }
blockInstTabCnt = 0; }

/**
* Clones this MiniTemplator object and resets the clone.
* This method is used to copy a MiniTemplator object.
* It is fast, because the template does not have to be parsed again,
* and the internal data structures that contain the parsed template
* information are shared among the clones.
* <p>This method is used by the {@link MiniTemplatorCache} class to
* clone the cached MiniTemplator objects.
*/
public MiniTemplator cloneReset() {
MiniTemplator m = new MiniTemplator();
m.mtp = mtp;                                            // the MiniTemplatorParser object is shared among the clones
m.charset = charset;
// (subtemplateBasePath does not have to be copied, because the subtemplates have already been read)
m.reset();
return m; }

/**
* Sets a template variable.
* <p>For variables that are used in blocks, the variable value
* must be set before <code>addBlock()</code> is called.
* @param  variableName   the name of the variable to be set. Case-insensitive.
* @param  variableValue  the new value of the variable. May be <code>null</code>.
* @param  isOptional     specifies whether an exception should be thrown when the
*    variable does not exist in the template. If <code>isOptional</code> is
*    <code>false</code> and the variable does not exist, an exception is thrown.
* @throws VariableNotDefinedException when no variable with the
*    specified name exists in the template and <code>isOptional</code> is <code>false</code>.
*/
public void setVariable (String variableName, String variableValue, boolean isOptional){
int varNo = mtp.lookupVariableName(variableName);

if (varNo == -1) {
   if (isOptional) 
	   return;
   //throw new VariableNotDefinedException(variableName);
   return;
   }
varValuesTab[varNo] = variableValue; 
}

/**
* Sets a template variable.
* <p>Convenience method for: <code>setVariable (variableName, variableValue, false)</code>
* @param  variableName    the name of the variable to be set. Case-insensitive.
* @param  variableValue   the new value of the variable. May be <code>null</code>.
* @throws VariableNotDefinedException when no variable with the
*    specified name exists in the template.
* @see #setVariable(String, String, boolean)
*/
public void setVariable (String variableName, String variableValue){
setVariable (variableName, variableValue, false); }

/**
* Sets an optional template variable.
* <p>Convenience method for: <code>setVariable (variableName, variableValue, true)</code>
* @param  variableName    the name of the variable to be set. Case-insensitive.
* @param  variableValue   the new value of the variable. May be <code>null</code>.
* @see #setVariable(String, String, boolean)
*/
public void setVariableOpt (String variableName, String variableValue) {
setVariable (variableName, variableValue, true); }

/**
* Sets a template variable to an escaped value.
* <p>Convenience method for: <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), isOptional)</code>
* @param  variableName   the name of the variable to be set.
* @param  variableValue  the new value of the variable. May be <code>null</code>.
*    Special HTML/XML characters are escaped.
* @param  isOptional     specifies whether an exception should be thrown when the
*    variable does not exist in the template. If <code>isOptional</code> is
*    <code>false</code> and the variable does not exist, an exception is thrown.
* @throws VariableNotDefinedException when no variable with the
*    specified name exists in the template and <code>isOptional</code> is <code>false</code>.
* @see #setVariable(String, String, boolean)
* @see #escapeHtml(String)
*/
public void setVariableEsc (String variableName, String variableValue, boolean isOptional)
   throws VariableNotDefinedException {
setVariable (variableName, escapeHtml(variableValue), isOptional); }

/**
* Sets a template variable to an escaped value.
* <p>Convenience method for: <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), false)</code>
* @param  variableName   the name of the variable to be set. Case-insensitive.
* @param  variableValue  the new value of the variable. May be <code>null</code>.
*    Special HTML/XML characters are escaped.
* @throws VariableNotDefinedException when no variable with the
*    specified name exists in the template.
* @see #setVariable(String, String, boolean)
* @see #escapeHtml(String)
*/
public void setVariableEsc (String variableName, String variableValue)
   throws VariableNotDefinedException {
setVariable (variableName, escapeHtml(variableValue), false); }

/**
* Sets an optional template variable to an escaped value.
* <p>Convenience method for: <code>setVariable (variableName, MiniTemplator.escapeHtml(variableValue), true)</code>
* @param  variableName   the name of the variable to be set. Case-insensitive.
* @param  variableValue  the new value of the variable. May be <code>null</code>.
*    Special HTML/XML characters are escaped.
* @see #setVariable(String, String, boolean)
* @see #escapeHtml(String)
*/
public void setVariableOptEsc (String variableName, String variableValue) {
setVariable (variableName, escapeHtml(variableValue), true); }

/**
* Checks whether a variable with the specified name exists within the template.
* @param  variableName  the name of the variable. Case-insensitive.
* @return <code>true</code> if the variable exists.<br>
*    <code>false</code> if no variable with the specified name exists in the template.
*/
public boolean variableExists (String variableName) {
return mtp.lookupVariableName(variableName) != -1; }

/**
* Returns a map with the names and current values of the template variables.
*/
public Map<String, String> getVariables() {
HashMap<String, String> map = new HashMap<String, String>(mtp.varTabCnt);
for (int varNo = 0; varNo < mtp.varTabCnt; varNo++)
   map.put (mtp.varTab[varNo], varValuesTab[varNo]);
return map; }

/**
* Adds an instance of a template block.
* <p>If the block contains variables, these variables must be set
* before the block is added.
* If the block contains subblocks (nested blocks), the subblocks
* must be added before this block is added.
* If multiple blocks exist with the specified name, an instance
* is added for each block occurrence.
* @param  blockName  the name of the block to be added. Case-insensitive.
* @param  isOptional specifies whether an exception should be thrown when the
*    block does not exist in the template. If <code>isOptional</code> is
*    <code>false</code> and the block does not exist, an exception is thrown.
* @throws BlockNotDefinedException when no block with the specified name
*    exists in the template and <code>isOptional</code> is <code>false</code>.
*/
public void addBlock (String blockName, boolean isOptional)
   throws BlockNotDefinedException {
int blockNo = mtp.lookupBlockName(blockName);
if(blockNo == -1) {
   if (isOptional) return;
   throw new BlockNotDefinedException(blockName); }
while (blockNo != -1) {
   addBlockByNo (blockNo);
   blockNo = mtp.blockTab[blockNo].nextWithSameName; }}

/**
* Adds an instance of a template block.
* <p>Convenience method for: <code>addBlock (blockName, false)</code>
* @param  blockName  the name of the block to be added. Case-insensitive.
* @throws BlockNotDefinedException when no block with the specified name
*    exists in the template.
* @see #addBlock(String, boolean)
*/
public void addBlock (String blockName)
   throws BlockNotDefinedException {
addBlock (blockName, false); }

/**
* Adds an instance of an optional template block.
* <p>Convenience method for: <code>addBlock (blockName, true)</code>
* @param  blockName  the name of the block to be added. Case-insensitive.
* @see #addBlock(String, boolean)
*/
public void addBlockOpt (String blockName) {
addBlock (blockName, true); }

private void addBlockByNo (int blockNo) {
MiniTemplatorParser.BlockTabRec btr = mtp.blockTab[blockNo];
BlockDynTabRec bdtr = blockDynTab[blockNo];
int blockInstNo = registerBlockInstance();
BlockInstTabRec bitr = blockInstTab[blockInstNo];
if (bdtr.firstBlockInstNo == -1)
   bdtr.firstBlockInstNo = blockInstNo;
if (bdtr.lastBlockInstNo != -1)
   blockInstTab[bdtr.lastBlockInstNo].nextBlockInstNo = blockInstNo; // set forward pointer of chain
bdtr.lastBlockInstNo = blockInstNo;
bitr.blockNo = blockNo;
bitr.instanceLevel = bdtr.instances++;
if (btr.parentBlockNo == -1)
   bitr.parentInstLevel = -1;
 else
   bitr.parentInstLevel = blockDynTab[btr.parentBlockNo].instances;
bitr.nextBlockInstNo = -1;
if (btr.blockVarCnt > 0)
   bitr.blockVarTab = new String[btr.blockVarCnt];
for (int blockVarNo=0; blockVarNo<btr.blockVarCnt; blockVarNo++) {  // copy instance variables for this block
   int varNo = btr.blockVarNoToVarNoMap[blockVarNo];
   bitr.blockVarTab[blockVarNo] = varValuesTab[varNo]; }}

//Returns the block instance number.
private int registerBlockInstance() {
int blockInstNo = blockInstTabCnt++;
if (blockInstTab == null)
   blockInstTab = new BlockInstTabRec[64];
if (blockInstTabCnt > blockInstTab.length)
   blockInstTab = (BlockInstTabRec[])MiniTemplatorParser.resizeArray(blockInstTab, 2*blockInstTabCnt);
blockInstTab[blockInstNo] = new BlockInstTabRec();
return blockInstNo; }

/**
* Checks whether a block with the specified name exists within the template.
* @param  blockName  the name of the block.
* @return <code>true</code> if the block exists.<br>
*    <code>false</code> if no block with the specified name exists in the template.
*/
public boolean blockExists (String blockName) {
return mtp.lookupBlockName(blockName) != -1; }

//--- output generation ----------------------------------------------

/**
* Generates the HTML page and writes it into a file.
* @param  outputFileName  name of the file to which the generated HTML page will be written.
* @throws IOException when an i/o error occurs while writing to the file.
*/
public void generateOutput (String outputFileName)
   throws IOException {
FileOutputStream stream = null;
OutputStreamWriter writer = null;
try {
   stream = new FileOutputStream(outputFileName);
   writer = new OutputStreamWriter(stream, charset);
   generateOutput (writer); }
 finally {
   if (writer != null) writer.close();
   if (stream != null) stream.close(); }}

public void generateOutput (OutputStream stream)throws IOException {
	OutputStreamWriter writer = null;
	try {
		writer = new OutputStreamWriter(stream,charset);
		generateOutput (writer); 
	}finally {
		
		if (writer != null) writer.flush();
		if (writer != null) writer.close();
		if (stream != null) stream.close(); 
	}
}

/**
* Generates the HTML page and writes it to a character stream.
* @param  outputWriter  a character stream (<code>writer</code>) to which
*    the HTML page will be written.
* @throws IOException when an i/o error occurs while writing to the stream.
*/
public void generateOutput (Writer outputWriter)throws IOException {
String s = generateOutput();
outputWriter.write (s); 
outputWriter.flush();
}

/**
* Generates the HTML page and returns it as a string.
* @return A string that contains the generated HTML page.
*/
public String generateOutput() {
if (blockDynTab[0].instances == 0)
   addBlockByNo (0);                          // add main block
for (int blockNo=0; blockNo<mtp.blockTabCnt; blockNo++) {
   BlockDynTabRec bdtr = blockDynTab[blockNo];
   bdtr.currBlockInstNo = bdtr.firstBlockInstNo; }
StringBuilder out = new StringBuilder();
writeBlockInstances (out, 0, -1);
return out.toString(); }

//Writes all instances of a block that are contained within a specific
//parent block instance.
//Called recursively.
private void writeBlockInstances (StringBuilder out, int blockNo, int parentInstLevel) {
BlockDynTabRec bdtr = blockDynTab[blockNo];
while (true) {
   int blockInstNo = bdtr.currBlockInstNo;
   if (blockInstNo == -1) break;
   BlockInstTabRec bitr = blockInstTab[blockInstNo];
   if (bitr.parentInstLevel < parentInstLevel)
      throw new Error();
   if (bitr.parentInstLevel > parentInstLevel) break;
   writeBlockInstance (out, blockInstNo);
   bdtr.currBlockInstNo = bitr.nextBlockInstNo; }}

private void writeBlockInstance (StringBuilder out, int blockInstNo) {
BlockInstTabRec bitr = blockInstTab[blockInstNo];
int blockNo = bitr.blockNo;
MiniTemplatorParser.BlockTabRec btr = mtp.blockTab[blockNo];
int tPos = btr.tPosContentsBegin;
int subBlockNo = blockNo + 1;
int varRefNo = btr.firstVarRefNo;
while (true) {
   int tPos2 = btr.tPosContentsEnd;
   int kind = 0;                              // assume end-of-block
   if (varRefNo != -1 && varRefNo < mtp.varRefTabCnt) { // check for variable reference
      MiniTemplatorParser.VarRefTabRec vrtr = mtp.varRefTab[varRefNo];
      if (vrtr.tPosBegin < tPos) {
         varRefNo++;
         continue; }
      if (vrtr.tPosBegin < tPos2) {
         tPos2 = vrtr.tPosBegin;
         kind = 1; }}
   if (subBlockNo < mtp.blockTabCnt) {        // check for subblock
      MiniTemplatorParser.BlockTabRec subBtr = mtp.blockTab[subBlockNo];
      if (subBtr.tPosBegin < tPos) {
         subBlockNo++;
         continue; }
      if (subBtr.tPosBegin < tPos2) {
         tPos2 = subBtr.tPosBegin;
         kind = 2; }}
   if (tPos2 > tPos)
      out.append (mtp.templateText.substring(tPos, tPos2));
   switch (kind) {
      case 0:                                 // end of block
         return;
      case 1: {                               // variable
         MiniTemplatorParser.VarRefTabRec vrtr = mtp.varRefTab[varRefNo];
         if (vrtr.blockNo != blockNo)
            throw new Error();
         String variableValue = bitr.blockVarTab[vrtr.blockVarNo];
         if (variableValue != null)
            out.append (variableValue);
         tPos = vrtr.tPosEnd;
         varRefNo++;
         break; }
      case 2: {                               // sub block
         MiniTemplatorParser.BlockTabRec subBtr = mtp.blockTab[subBlockNo];
         if (subBtr.parentBlockNo != blockNo)
            throw new Error();
         writeBlockInstances (out, subBlockNo, bitr.instanceLevel);  // recursive call
         tPos = subBtr.tPosEnd;
         subBlockNo++;
         break; }}}}

//--- general utility routines ---------------------------------------

//Reads the contents of a file into a string variable.
private String readFileIntoString (String fileName)
   throws IOException {
FileInputStream stream = null;
InputStreamReader reader = null;
try {
   stream = new FileInputStream(fileName);
   reader = new InputStreamReader(stream, charset);
   return readStreamIntoString(reader); }
 finally {
   if (reader != null) reader.close();
   if (stream != null) stream.close(); }}

//Reads the contents of a stream into a string variable.
private static String readStreamIntoString (Reader reader)
   throws IOException {
StringBuilder s = new StringBuilder();
char a[] = new char[0x10000];
while (true) {
   int l = reader.read(a);
   if (l == -1) break;
   if (l <= 0) throw new IOException();
   s.append (a, 0, l); }
return s.toString(); }

/**
* Escapes special HTML characters.
* Replaces the characters &lt;, &gt;, &amp;, ' and " by their corresponding
* HTML/XML character entity codes.
* @param  s  the input string.
* @return the escaped output string.
*/
public static String escapeHtml (String s) {
// (The code of this method is a bit redundant in order to optimize speed)
if (s == null) return null;
int sLength = s.length();
boolean found = false;
int p;
loop1:
for (p=0; p<sLength; p++) {
   switch (s.charAt(p)) {
      case '<': case '>': case '&': case '\'': case '"': found = true; break loop1; }}
if (!found) return s;
StringBuilder sb = new StringBuilder(sLength+16);
sb.append (s.substring(0, p));
for (; p<sLength; p++) {
   char c = s.charAt(p);
   switch (c) {
      case '<':  sb.append ("&lt;"); break;
      case '>':  sb.append ("&gt;"); break;
      case '&':  sb.append ("&amp;"); break;
      case '\'': sb.append ("&#39;"); break;
      case '"':  sb.append ("&#34;"); break;
      default:   sb.append (c); }}
return sb.toString(); }

} // End class MiniTemplator

//====================================================================================================================

//MiniTemplatorParser is an immutable object that contains the parsed template text.
class MiniTemplatorParser {

//--- constants ------------------------------------------------------

private static final int     maxNestingLevel = 20;         // maximum number of block nestings
private static final int     maxCondLevels = 20;           // maximum number of nested conditional commands ($if)
private static final int     maxInclTemplateSize = 1000000; // maximum length of template string when including subtemplates

//--- nested classes -------------------------------------------------

public static class VarRefTabRec {                         // variable reference table record structure
int                       varNo;                        // variable no
int                       tPosBegin;                    // template position of begin of variable reference
int                       tPosEnd;                      // template position of end of variable reference
int                       blockNo;                      // block no of the (innermost) block that contains this variable reference
int                       blockVarNo; }                 // block variable no. Index into BlockInstTab.BlockVarTab
public static class BlockTabRec {                          // block table record structure
String                    blockName;                    // block name
int                       nextWithSameName;             // block no of next block with same name or -1 (blocks are backward linked related to their position within the template)
int                       tPosBegin;                    // template position of begin of block
int                       tPosContentsBegin;            // template pos of begin of block contents
int                       tPosContentsEnd;              // template pos of end of block contents
int                       tPosEnd;                      // template position of end of block
int                       nestingLevel;                 // block nesting level
int                       parentBlockNo;                // block no of parent block
boolean                   definitionIsOpen;             // true while $BeginBlock processed but no $EndBlock
int                       blockVarCnt;                  // number of variables in block
int[]                     blockVarNoToVarNoMap;         // maps block variable numbers to variable numbers
int                       firstVarRefNo;                // variable reference no of first variable of this block or -1
boolean                   dummy; }                      // true if this is a dummy block that will never be included in the output

//--- variables ------------------------------------------------------

public  String               templateText;                 // contents of the template file
private HashSet<String>      conditionFlags;               // set of the condition flags, converted to uppercase

public  String[]             varTab;                       // variables table, contains variable names, array index is variable no
public  int                  varTabCnt;                    // no of entries used in VarTab
private HashMap<String,Integer> varNameToNoMap;            // maps variable names to variable numbers
public  VarRefTabRec[]       varRefTab;                    // variable references table
// Contains an entry for each variable reference in the template. Ordered by templatePos.
public  int                  varRefTabCnt;                 // no of entries used in VarRefTab

public  BlockTabRec[]        blockTab;                     // Blocks table, array index is block no
// Contains an entry for each block in the template. Ordered by tPosBegin.
public  int                  blockTabCnt;                  // no of entries used in BlockTab
private HashMap<String,Integer> blockNameToNoMap;          // maps block names to block numbers

//The following variables are only used temporarilly during parsing of the template.
private int                  currentNestingLevel;          // current block nesting level during parsing
private int[]                openBlocksTab;                // indexed by the block nesting level
// During parsing, this table contains the block numbers of the open parent blocks (nested outer blocks).
private int                  condLevel;                    // current nesting level of conditional commands ($if), -1 = main level
private boolean[]            condEnabled;                  // enabled/disables state for the conditions of each level
private boolean[]            condPassed;                   // true if an enabled condition clause has already been processed (separate for each level)
private MiniTemplator        miniTemplator;                // the MiniTemplator who created this parser object
// The reference to the MiniTemplator object is only used to call MiniTemplator.loadSubtemplate().
private boolean              resumeCmdParsingFromStart;    // true = resume command parsing from the start position of the last command

//--- constructor ----------------------------------------------------

//(The MiniTemplator object is only passed to the parser, because the
//parser needs to call MiniTemplator.loadSubtemplate() to load subtemplates.)
public MiniTemplatorParser (String templateText, Set<String> conditionFlags, MiniTemplator miniTemplator)
   throws MiniTemplator.TemplateSyntaxException {
this.templateText = templateText;
this.conditionFlags = createConditionFlagsSet(conditionFlags);
this.miniTemplator = miniTemplator;
parseTemplate();
this.miniTemplator = null; }

private HashSet<String> createConditionFlagsSet (Set<String> flags) {
if (flags == null || flags.isEmpty()) return null;
HashSet<String> flags2 = new HashSet<String>(flags.size());
for (String flag : flags) flags2.add (flag.toUpperCase());
return flags2; }

//--- template parsing -----------------------------------------------

private void parseTemplate()
   throws MiniTemplator.TemplateSyntaxException {
initParsing();
beginMainBlock();
parseTemplateCommands();
endMainBlock();
checkBlockDefinitionsComplete();
if (condLevel != -1) throw new MiniTemplator.TemplateSyntaxException ("$if without matching $endIf.");
parseTemplateVariables();
associateVariablesWithBlocks();
terminateParsing(); }

private void initParsing() {
varTab = new String[64];
varTabCnt = 0;
varNameToNoMap = new HashMap<String,Integer>();
varRefTab = new VarRefTabRec[64];
varRefTabCnt = 0;
blockTab = new BlockTabRec[16];
blockTabCnt = 0;
currentNestingLevel = 0;
blockNameToNoMap = new HashMap<String,Integer>();
openBlocksTab = new int[maxNestingLevel+1];
condLevel = -1;
condEnabled = new boolean[maxCondLevels];
condPassed = new boolean[maxCondLevels]; }

private void terminateParsing() {
openBlocksTab = null; }

//Registers the main block.
//The main block is an implicitly defined block that covers the whole template.
private void beginMainBlock() {
int blockNo = registerBlock(null);                      // =0
BlockTabRec btr = blockTab[blockNo];
btr.tPosBegin = 0;
btr.tPosContentsBegin = 0;
openBlocksTab[currentNestingLevel] = blockNo;
currentNestingLevel++; }

//Completes the main block registration.
private void endMainBlock() {
BlockTabRec btr = blockTab[0];
btr.tPosContentsEnd = templateText.length();
btr.tPosEnd = templateText.length();
btr.definitionIsOpen = false;
currentNestingLevel--; }

//Parses commands within the template in the format "<!-- $command parameters -->".
private void parseTemplateCommands()
   throws MiniTemplator.TemplateSyntaxException {
int p = 0;
//while (true) {
//   int p0 = templateText.indexOf("<!--", p);
//   if (p0 == -1) break;
//   conditionalExclude (p, p0);
//   p = templateText.indexOf("-->", p0);
//   if (p == -1) throw new MiniTemplator.TemplateSyntaxException("Invalid HTML comment in template at offset " + p0 + ".");
//   p += 3;
//   String cmdLine = templateText.substring(p0+4, p-3);
//   resumeCmdParsingFromStart = false;
//   processTemplateCommand (cmdLine, p0, p);
//   if (resumeCmdParsingFromStart) p = p0; 
//   }
}

private void processTemplateCommand (String cmdLine, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
int p0 = skipBlanks(cmdLine, 0);
if (p0 >= cmdLine.length()) return;
int p = skipNonBlanks(cmdLine, p0);
String cmd = cmdLine.substring(p0, p);
String parms = cmdLine.substring(p);
/* select */
   if (cmd.equalsIgnoreCase("$beginBlock"))
      processBeginBlockCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$endBlock"))
      processEndBlockCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$include"))
      processIncludeCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$if"))
      processIfCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$elseIf"))
      processElseIfCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$else"))
      processElseCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else if (cmd.equalsIgnoreCase("$endIf"))
      processEndIfCmd (parms, cmdTPosBegin, cmdTPosEnd);
   else {
      if (cmd.startsWith("$") && !cmd.startsWith("${"))
         throw new MiniTemplator.TemplateSyntaxException("Unknown command \"" + cmd + "\" in template at offset " + cmdTPosBegin + "."); }}

//Processes the $beginBlock command.
private void processBeginBlockCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) return;
int p0 = skipBlanks(parms, 0);
if (p0 >= parms.length())
   throw new MiniTemplator.TemplateSyntaxException("Missing block name in $BeginBlock command in template at offset " + cmdTPosBegin + ".");
int p = skipNonBlanks(parms, p0);
String blockName = parms.substring(p0, p);
if (!isRestOfStringBlank(parms, p))
   throw new MiniTemplator.TemplateSyntaxException("Extra parameter in $BeginBlock command in template at offset " + cmdTPosBegin + ".");
int blockNo = registerBlock(blockName);
BlockTabRec btr = blockTab[blockNo];
btr.tPosBegin = cmdTPosBegin;
btr.tPosContentsBegin = cmdTPosEnd;
openBlocksTab[currentNestingLevel] = blockNo;
currentNestingLevel++;
if (currentNestingLevel > maxNestingLevel)
   throw new MiniTemplator.TemplateSyntaxException("Block nesting overflow for block \"" + blockName + "\" in template at offset " + cmdTPosBegin + "."); }

//Processes the $endBlock command.
private void processEndBlockCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) return;
int p0 = skipBlanks(parms, 0);
if (p0 >= parms.length())
   throw new MiniTemplator.TemplateSyntaxException("Missing block name in $EndBlock command in template at offset " + cmdTPosBegin + ".");
int p = skipNonBlanks(parms, p0);
String blockName = parms.substring(p0, p);
if (!isRestOfStringBlank(parms, p))
   throw new MiniTemplator.TemplateSyntaxException("Extra parameter in $EndBlock command in template at offset " + cmdTPosBegin + ".");
int blockNo = lookupBlockName(blockName);
if (blockNo == -1)
   throw new MiniTemplator.TemplateSyntaxException("Undefined block name \"" + blockName + "\" in $EndBlock command in template at offset " + cmdTPosBegin + ".");
currentNestingLevel--;
BlockTabRec btr = blockTab[blockNo];
if (!btr.definitionIsOpen) throw new MiniTemplator.TemplateSyntaxException("Multiple $EndBlock command for block \"" + blockName + "\" in template at offset " + cmdTPosBegin + ".");
if (btr.nestingLevel != currentNestingLevel) throw new MiniTemplator.TemplateSyntaxException("Block nesting level mismatch at $EndBlock command for block \"" + blockName + "\" in template at offset " + cmdTPosBegin + ".");
btr.tPosContentsEnd = cmdTPosBegin;
btr.tPosEnd = cmdTPosEnd;
btr.definitionIsOpen = false; }

//Returns the block number of the newly registered block.
private int registerBlock (String blockName) {
int blockNo = blockTabCnt++;
if (blockTabCnt > blockTab.length)
   blockTab = (BlockTabRec[])resizeArray(blockTab, 2*blockTabCnt);
BlockTabRec btr = new BlockTabRec();
blockTab[blockNo] = btr;
btr.blockName = blockName;
if (blockName != null)
   btr.nextWithSameName = lookupBlockName(blockName);
 else
   btr.nextWithSameName = -1;
btr.nestingLevel = currentNestingLevel;
if (currentNestingLevel > 0)
   btr.parentBlockNo = openBlocksTab[currentNestingLevel-1];
 else
   btr.parentBlockNo = -1;
btr.definitionIsOpen = true;
btr.blockVarCnt = 0;
btr.firstVarRefNo = -1;
btr.blockVarNoToVarNoMap = new int[32];
btr.dummy = false;
if (blockName != null)
   blockNameToNoMap.put (blockName.toUpperCase(), new Integer(blockNo));
return blockNo; }

//Registers a dummy block to exclude a range within the template text.
private void excludeTemplateRange (int tPosBegin, int tPosEnd) {
if (blockTabCnt > 0) {
   // Check whether we can extend the previous block.
   BlockTabRec btr = blockTab[blockTabCnt-1];
   if (btr.dummy && btr.tPosEnd == tPosBegin) {
      btr.tPosContentsEnd = tPosEnd;
      btr.tPosEnd = tPosEnd;
      return; }}
int blockNo = registerBlock(null);
BlockTabRec btr = blockTab[blockNo];
btr.tPosBegin = tPosBegin;
btr.tPosContentsBegin = tPosBegin;
btr.tPosContentsEnd = tPosEnd;
btr.tPosEnd = tPosEnd;
btr.definitionIsOpen = false;
btr.dummy = true; }

//Checks that all block definitions are closed.
private void checkBlockDefinitionsComplete()
   throws MiniTemplator.TemplateSyntaxException {
for (int blockNo=0; blockNo<blockTabCnt; blockNo++) {
   BlockTabRec btr = blockTab[blockNo];
   if (btr.definitionIsOpen)
      throw new MiniTemplator.TemplateSyntaxException("Missing $EndBlock command in template for block \"" + btr.blockName + "\"."); }
if (currentNestingLevel != 0)
   throw new MiniTemplator.TemplateSyntaxException("Block nesting level error at end of template."); }

//Processes the $include command.
private void processIncludeCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
if (conditionalExclude(cmdTPosBegin, cmdTPosEnd)) return;
int p0 = skipBlanks(parms, 0);
if (p0 >= parms.length())
   throw new MiniTemplator.TemplateSyntaxException("Missing subtemplate name in $Include command in template at offset " + cmdTPosBegin + ".");
int p;
if (parms.charAt(p0) == '"') {                          // subtemplate name is quoted
   p0++;
   p = parms.indexOf('"', p0);
   if (p == -1) throw new MiniTemplator.TemplateSyntaxException("Missing closing quote for subtemplate name in $Include command in template at offset " + cmdTPosBegin + "."); }
 else
   p = skipNonBlanks(parms, p0);
String subtemplateName = parms.substring(p0, p);
p++;
if (!isRestOfStringBlank(parms, p))
   throw new MiniTemplator.TemplateSyntaxException("Extra parameter in $Include command in template at offset " + cmdTPosBegin + ".");
insertSubtemplate (subtemplateName, cmdTPosBegin, cmdTPosEnd); }

private void insertSubtemplate (String subtemplateName, int tPos1, int tPos2) {
if (templateText.length() > maxInclTemplateSize)
   throw new RuntimeException("Subtemplate include aborted because the internal template string is longer than "+maxInclTemplateSize+" characters.");
String subtemplate;
try {
   subtemplate = miniTemplator.loadSubtemplate(subtemplateName); }
 catch (IOException e) {
   throw new RuntimeException("Error while loading subtemplate \""+subtemplateName+"\"", e); }
// (Copying the template to insert a subtemplate is a bit slow. In a future implementation of MiniTemplator,
// a table could be used that contains references to the string fragments.)
StringBuilder s = new StringBuilder(templateText.length()+subtemplate.length());
s.append (templateText, 0, tPos1);
s.append (subtemplate);
s.append (templateText, tPos2, templateText.length());
templateText = s.toString();
resumeCmdParsingFromStart = true; }

//--- Conditional commands -----------------------------------------------------

//Returns the enabled/disabled state of the condition at level condLevel2.
private boolean isCondEnabled (int condLevel2) {
if (condLevel2 < 0) return true;
return condEnabled[condLevel2]; }

//Excludes a text range and returns true, if the current condition is disabled.
private boolean conditionalExclude (int tPosBegin, int tPosEnd) {
if (isCondEnabled(condLevel)) return false;
excludeTemplateRange (tPosBegin, tPosEnd);
return true; }

//Evaluates a condition expression of a conditional command, by comparing the
//flags in the expression with the flags in TemplateSpecification.conditionFlags.
//Returns true the condition is met.
private boolean evaluateConditionFlags (String flags) {
if (conditionFlags == null) return false;
int p = 0;
while (true) {
   p = skipBlanks(flags, p);
   if (p >= flags.length()) break;
   boolean complement = false;
   if (flags.charAt(p) == '!') {
      complement = true; p++; }
   p = skipBlanks(flags, p);
   if (p >= flags.length()) break;
   int p0 = p;
   p = skipNonBlanks(flags, p0+1);
   String flag = flags.substring(p0, p).toUpperCase();
   if (conditionFlags.contains(flag) ^ complement) return true; }
return false; }

//Processes the $if command.
private void processIfCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
excludeTemplateRange (cmdTPosBegin, cmdTPosEnd);
if (condLevel >= maxCondLevels-1) throw new MiniTemplator.TemplateSyntaxException ("Too many nested $if commands.");
condLevel++;
boolean enabled = isCondEnabled(condLevel-1) && evaluateConditionFlags(parms);
condEnabled[condLevel] = enabled;
condPassed[condLevel] = enabled; }

//Processes the $elseIf command.
private void processElseIfCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
excludeTemplateRange (cmdTPosBegin, cmdTPosEnd);
if (condLevel < 0) throw new MiniTemplator.TemplateSyntaxException ("$elseIf without matching $if.");
boolean enabled = isCondEnabled(condLevel-1) && !condPassed[condLevel] && evaluateConditionFlags(parms);
condEnabled[condLevel] = enabled;
if (enabled) condPassed[condLevel] = true; }

//Processes the $else command.
private void processElseCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
excludeTemplateRange (cmdTPosBegin, cmdTPosEnd);
if (parms.trim().length() != 0) throw new MiniTemplator.TemplateSyntaxException ("Invalid parameters for $else command.");
if (condLevel < 0) throw new MiniTemplator.TemplateSyntaxException ("$else without matching $if.");
boolean enabled = isCondEnabled(condLevel-1) && !condPassed[condLevel];
condEnabled[condLevel] = enabled;
if (enabled) condPassed[condLevel] = true; }

//Processes the $endIf command.
private void processEndIfCmd (String parms, int cmdTPosBegin, int cmdTPosEnd)
   throws MiniTemplator.TemplateSyntaxException {
excludeTemplateRange (cmdTPosBegin, cmdTPosEnd);
if (parms.trim().length() != 0) throw new MiniTemplator.TemplateSyntaxException ("Invalid parameters for $endIf command.");
if (condLevel < 0) throw new MiniTemplator.TemplateSyntaxException ("$endif without matching $if.");
condLevel--; }

//------------------------------------------------------------------------------

//Associates variable references with blocks.
private void associateVariablesWithBlocks() {
int varRefNo = 0;
int activeBlockNo = 0;
int nextBlockNo = 1;
while (varRefNo < varRefTabCnt) {
   VarRefTabRec vrtr = varRefTab[varRefNo];
   int varRefTPos = vrtr.tPosBegin;
   int varNo = vrtr.varNo;
   if (varRefTPos >= blockTab[activeBlockNo].tPosEnd) {
      activeBlockNo = blockTab[activeBlockNo].parentBlockNo;
      continue; }
   if (nextBlockNo < blockTabCnt && varRefTPos >= blockTab[nextBlockNo].tPosBegin) {
      activeBlockNo = nextBlockNo;
      nextBlockNo++;
      continue; }
   BlockTabRec btr = blockTab[activeBlockNo];
   if (varRefTPos < btr.tPosBegin) throw new Error();
   int blockVarNo = btr.blockVarCnt++;
   if (btr.blockVarCnt > btr.blockVarNoToVarNoMap.length)
      btr.blockVarNoToVarNoMap = (int[])resizeArray(btr.blockVarNoToVarNoMap, 2*btr.blockVarCnt);
   btr.blockVarNoToVarNoMap[blockVarNo] = varNo;
   if (btr.firstVarRefNo == -1) btr.firstVarRefNo = varRefNo;
   vrtr.blockNo = activeBlockNo;
   vrtr.blockVarNo = blockVarNo;
   varRefNo++; }}

//Parses variable references within the template in the format "${VarName}" .
private void parseTemplateVariables()
   throws MiniTemplator.TemplateSyntaxException {
int p = 0;
while (true) {
   p = templateText.indexOf("${", p);
   if (p == -1) break;
   int p0 = p;
   p = templateText.indexOf("}", p);
   if (p == -1) throw new MiniTemplator.TemplateSyntaxException("Invalid variable reference in template at offset " + p0 + ".");
   p++;
   String varName = templateText.substring(p0+2, p-1).trim();
  // if (varName.length() == 0) throw new MiniTemplator.TemplateSyntaxException("Empty variable name in template at offset " + p0 + ".");
   registerVariableReference (varName, p0, p); }}

private void registerVariableReference (String varName, int tPosBegin, int tPosEnd) {
int varNo;
varNo = lookupVariableName(varName);
if (varNo == -1)
   varNo = registerVariable(varName);
int varRefNo = varRefTabCnt++;
if (varRefTabCnt > varRefTab.length)
   varRefTab = (VarRefTabRec[])resizeArray(varRefTab, 2*varRefTabCnt);
VarRefTabRec vrtr = new VarRefTabRec();
varRefTab[varRefNo] = vrtr;
vrtr.tPosBegin = tPosBegin;
vrtr.tPosEnd = tPosEnd;
vrtr.varNo = varNo; }

//Returns the variable number of the newly registered variable.
private int registerVariable (String varName) {
int varNo = varTabCnt++;
if (varTabCnt > varTab.length)
   varTab = (String[])resizeArray(varTab, 2*varTabCnt);
varTab[varNo] = varName;
varNameToNoMap.put (varName.toUpperCase(), new Integer(varNo));
return varNo; }

//--- name lookup routines -------------------------------------------

//Maps variable name to variable number.
//Returns -1 if the variable name is not found.
public int lookupVariableName (String varName) {
Integer varNoWrapper = varNameToNoMap.get(varName.toUpperCase());
if (varNoWrapper == null) return -1;
int varNo = varNoWrapper.intValue();
return varNo; }

//Maps block name to block number.
//If there are multiple blocks with the same name, the block number of the last
//registered block with that name is returned.
//Returns -1 if the block name is not found.
public int lookupBlockName (String blockName) {
Integer blockNoWrapper = blockNameToNoMap.get(blockName.toUpperCase());
if (blockNoWrapper == null) return -1;
int blockNo = blockNoWrapper.intValue();
return blockNo; }

//--- general utility routines ---------------------------------------

//Reallocates an array with a new size and copies the contents
//of the old array to the new array.
public static Object resizeArray (Object oldArray, int newSize) {
int oldSize = java.lang.reflect.Array.getLength(oldArray);
Class elementType = oldArray.getClass().getComponentType();
Object newArray = java.lang.reflect.Array.newInstance(
      elementType, newSize);
int preserveLength = Math.min(oldSize, newSize);
if (preserveLength > 0)
   System.arraycopy (oldArray, 0, newArray, 0, preserveLength);
return newArray; }

//Skips blanks (white space) in string s starting at position p.
private static int skipBlanks (String s, int p) {
while (p < s.length() && Character.isWhitespace(s.charAt(p))) p++;
return p; }

//Skips non-blanks (no-white space) in string s starting at position p.
private static int skipNonBlanks (String s, int p) {
while (p < s.length() && !Character.isWhitespace(s.charAt(p))) p++;
return p; }

//Returns true if string s is blank (white space) from position p to the end.
public static boolean isRestOfStringBlank (String s, int p) {
return skipBlanks(s, p) >= s.length(); }

} // End class MiniTemplatorParser
